package edu.umb.cs.antmanager.manager;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.xmlbeans.XmlException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import com.healthmarketscience.rmiio.RemoteInputStream;
import com.healthmarketscience.rmiio.RemoteInputStreamClient;

import edu.umb.cs.antmanager.common.MachineStatus;
import edu.umb.cs.antmanager.common.ManagerInterface;
import edu.umb.cs.antmanager.domain.MachineType;
import edu.umb.cs.antmanager.domain.TestType;
import edu.umb.cs.antmanager.domain.TestsDocument;
import edu.umb.cs.antmanager.manager.TestInfo.TestStatus;

import static edu.umb.cs.antmanager.common.Constants.*;

/**
 * Main class of the manager, for task distribution and result collection
 * 
 * @author 2009CS682-3UMB_AntManager
 * 
 */
public class AntManager implements ManagerInterface {

	private static final long serialVersionUID = 1L;
	private static AntManager server;
	private static String testFile;
	private static String testFilter;
	private ReentrantLock lock;
	private Condition idleMachineCondition;
	private HashMap<TestType, TestInfo> testsStatus;
	private HashMap<MachineType, String> machinesStatus;

	/**
	 * Singleton factory method for getting current running manager, if not
	 * exist create a new one.
	 * 
	 * @return Instance of <tt>AntManager</tt>
	 * @throws RemoteException
	 */
	public static synchronized AntManager getManager() {
		if (server == null)
			server = new AntManager();
		return server;
	}

	/**
	 * Create a manager
	 */
	private AntManager() {
		lock = new ReentrantLock();
		idleMachineCondition = lock.newCondition();
		machinesStatus = new HashMap<MachineType, String>();
		testsStatus = new HashMap<TestType, TestInfo>();
		// initialize the machines' status
		for (MachineType m : ResourceManager.getResourceManager().getMachines()) {
			machinesStatus.put(m, null);
		}
		try {
			System.setProperty("java.rmi.server.hostname", InetAddress
					.getLocalHost().getHostName());
		} catch (UnknownHostException e1) {
			System.out.println("Cannot get host name");
			System.exit(0);
		}
		try {
			UnicastRemoteObject.exportObject(this, MANAGER_CALLBACK_PORT);
		} catch (RemoteException e) {
			e.printStackTrace();
			System.exit(0);
		}
	}

	/**
	 * Application main
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		if (args.length <= 0 || args[0].equals("-h")) {
			printUsage();
			System.exit(0);
		}
		String machineFile = ANT_MANAGER_HOME + "machines.xml";
		testFile = ANT_MANAGER_HOME + "testList.xml";
		String machineFilter = null;
		testFilter = null;
		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("-m"))
				machineFile = args[++i];
			else if (args[i].equals("-t"))
				testFile = args[++i];
			else if (args[i].equals("-mf"))
				machineFilter = args[++i];
			else if (args[i].equals("-tf"))
				testFilter = args[++i];
			else if (args[i].equals("-c"))
				System.setProperty(ANTMANAGER_TEST_CONTINUE, String
						.valueOf(true));
		}
		ResourceManager.getResourceManager().LoadMachines(machineFile,
				machineFilter);
		AntManager.getManager().startTest();
	}

	/**
	 * Print out application parameters
	 */
	private static void printUsage() {
		System.out.println("Usage:");
		System.out
				.println("AntManager -m machines.xml -t testList.xml [-mf m1,m2,m3] [-tf t1,t2,t3] [-c]");
		System.out.println("OPTIONS:");
		System.out.println("\t-m\t Specify the machine file");
		System.out.println("\t-t\t Specify the test list file");
		System.out
				.println("\t-mf\t Specify a subset of machines, separate by ','");
		System.out
				.println("\t-tf\t Specify a subset of tests, separate by ','");
		System.out.println("\t-c\t Continue last test");
		System.out.println("\t-h\t Display this message");
	}

	/**
	 * Load test list from XML
	 * 
	 * @param filename
	 *            XML file contains the test list
	 * 
	 * @param filter
	 *            Test name filter, used to specify a subset of test
	 */
	private void loadTests(String filename, String filter) {
		try {
			TestsDocument testsDoc = TestsDocument.Factory.parse(new File(
					filename));
			boolean useFilter = true;
			if (filter == null) {
				filter = "";
				useFilter = false;
			}
			ArrayList<String> flst = new ArrayList<String>(Arrays.asList(filter
					.split(",")));
			ArrayList<String> finishedTests = new ArrayList<String>();
			if (Boolean.getBoolean(ANTMANAGER_TEST_CONTINUE)) {
				try {
					XMLReader xr = XMLReaderFactory.createXMLReader();
					xr.setContentHandler(new SummaryHandler(finishedTests));
					xr.parse(new InputSource(new FileInputStream(
							ANT_MANAGER_HOME + "summary.xml")));
				} catch (SAXException e) {
					e.printStackTrace();
				}
			}

			for (TestType t : testsDoc.getTests().getTestArray()) {
				if (useFilter && !flst.contains(t.getName()))
					continue;
				TestInfo info = new TestInfo(t);
				if (finishedTests.contains(t.getName()))
					info.setStatus(TestStatus.FINISHED);
				testsStatus.put(t, info);

			}

		} catch (XmlException e) {
			System.out.println(filename + " is not a valid test list!");
			exit();
		} catch (IOException e) {
			System.out.println("Cannot open " + filename);
			exit();
		}
	}

	/**
	 * Check if all test can by done by the provided Agents
	 */
	private void evaluateTests() {
		for (TestType t : testsStatus.keySet()) {
			if (ResourceManager.getResourceManager().findBestMachine(
					t.getRequirements(), machinesStatus.keySet()) == null) {
				System.out
						.print("Cannot find a machine to run: " + t.getName());
				exit();
			}
		}
	}

	/**
	 * Check if all Agents are available
	 */
	private void sysTest() {
		for (MachineType m : machinesStatus.keySet()) {
			new AgentConnRunnable(m.getName(), this, TASK_TYPE_STATUS).run();
		}
		boolean fail = false;
		for (Entry<MachineType, String> entry : machinesStatus.entrySet()) {
			if (entry.getValue() == null
					|| !entry.getValue().equals(STATUS_IDLE)) {
				System.out.println(entry.getKey().getName() + " is "
						+ entry.getValue());
				fail = true;
			}
		}
		if (fail) {
			System.out.println("System check fails!");
			exit();
		}
	}

	/**
	 * Try to do all the tests
	 */
	private void startTest() {
		sysTest();
		loadTests(testFile, testFilter);
		evaluateTests();
		lock.lock();
		try {
			while (true) {
				if (isAllTestFinished()) {
					System.out.println("All tests done!!");
					exit();
				}
				for (MachineType m : getIdleMachineList()) {
					List<TestType> untestedTestList = getUntestedTestList();
					TestType t = ResourceManager.getResourceManager()
							.findBestTest(untestedTestList, m.getResources());
					if (t != null) {
						System.out.println("Execute " + t.getName()
								+ " on Agent " + m.getName());
						new Thread(new AgentConnRunnable(m.getName(), this,
								TASK_TYPE_EXECUTE, t)).start();
						machinesStatus.put(m, STATUS_BUSY);
						testsStatus.get(t).setStatus(
								TestInfo.TestStatus.RUNNING);
						testsStatus.get(t).setStartTime(new Date());
						testsStatus.get(t).setAgentName(m.getName());
					}
				}
				System.out.println("Wait for idle agent");
				idleMachineCondition.await();
			}
		} catch (InterruptedException e) {
			// This should not happen
			System.out.println("Manager thread interrupted");
		} finally {
			lock.unlock();
		}
	}

	/**
	 * update a test's status
	 * 
	 * @param t
	 *            <tt>TestType</tt> test
	 * @param status
	 *            status of test
	 */
	protected void updateTestStatus(TestType t, TestInfo.TestStatus status) {
		lock.lock();
		try {
			testsStatus.get(t).setStatus(status);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Check if all the tests are done
	 * 
	 * @return true, if all tests' is finished; otherwise false.
	 */
	private boolean isAllTestFinished() {
		lock.lock();
		try {
			for (Map.Entry<TestType, TestInfo> e : testsStatus.entrySet()) {
				if (!e.getValue().getStatus().equals(
						TestInfo.TestStatus.FINISHED))
					return false;
			}
			return true;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Get all untested tests
	 * 
	 * @return A list contains all untested tests
	 */
	private List<TestType> getUntestedTestList() {
		lock.lock();
		try {
			List<TestType> testList = new ArrayList<TestType>();
			for (Map.Entry<TestType, TestInfo> e : testsStatus.entrySet()) {
				if (e.getValue().getStatus().equals(
						TestInfo.TestStatus.UNTESTED))
					testList.add(e.getKey());
			}
			return testList;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Get all idle machines
	 * 
	 * @return a list contains all idle machines.
	 */
	private List<MachineType> getIdleMachineList() {
		lock.lock();
		try {
			List<MachineType> machineList = new ArrayList<MachineType>();
			for (Map.Entry<MachineType, String> e : machinesStatus.entrySet()) {
				if (e.getValue() != null && e.getValue().equals(STATUS_IDLE))
					machineList.add(e.getKey());
			}
			return machineList;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Get <tt>MachineType<tt> from name
	 * 
	 * @param machineName
	 *            machine's host name
	 * @return <tt>MachineType<tt> with the host name
	 */
	private MachineType getMachineByName(String machineName) {
		for (MachineType m : machinesStatus.keySet()) {
			if (m.getName().compareToIgnoreCase(machineName) == 0)
				return m;
		}
		return null;
	}

	/**
	 * Get <tt>TestType</tt> from name
	 * 
	 * @param testName
	 *            test name
	 * @return <tt>TestType</tt> with the test name
	 */
	private TestType getTestByName(String testName) {
		for (TestType t : testsStatus.keySet()) {
			if (t.getName().equals(testName))
				return t;
		}
		return null;
	}

	/**
	 * Terminate the manager
	 */
	public void exit() {
		try {
			UnicastRemoteObject.unexportObject(this, true);
		} catch (NoSuchObjectException e) {
		}
		System.exit(0);
	}

	@Override
	public void addTestLog(RemoteInputStream log, MachineStatus agentStatus)
			throws RemoteException {
		lock.lock();
		try {
			TestType test = getTestByName(agentStatus.getRunningTest()
					.getName());
			testsStatus.get(test).setStatus(TestInfo.TestStatus.FINISHED);
			testsStatus.get(test).setEndTime(new Date());
			System.out.println(testsStatus.get(test).getTest().getName()
					+ " was finished!");
			SaxLogBuilder.getLogBuilder().addTestResult(
					RemoteInputStreamClient.wrap(log), testsStatus.get(test));
			updateMachineStatus(agentStatus.getMachineName(), STATUS_IDLE);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}

	}

	/**
	 * Update the agent's status for <tt>AgentConnRunnable</tt> to call back.
	 * 
	 * @param agentName
	 *            agent host name
	 * @param status
	 *            agent's status
	 */
	protected void updateMachineStatus(String agentName, String status) {
		lock.lock();
		try {
			machinesStatus.put(getMachineByName(agentName), status);
			if (status != null && status.equals(STATUS_IDLE)) {
				// if the machine's status is idle wake up the test loop
				System.out.println("Notify idle Agent "
						+ agentName.toUpperCase());
				idleMachineCondition.signalAll();
			}
		} finally {
			lock.unlock();
		}
	}

}
